<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="description" content="迎着朝阳的博客" />
  
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    kafka学习笔记（三） |  迎着朝阳
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="https://dxysun.com/static/yan.png" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  
<script>
var _hmt = _hmt || [];
(function() {
	var hm = document.createElement("script");
	hm.src = "https://hm.baidu.com/hm.js?aa994a8d65700b8835787dd39d079d7e";
	var s = document.getElementsByTagName("script")[0]; 
	s.parentNode.insertBefore(hm, s);
})();
</script>


</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-javaForKafka3"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  kafka学习笔记（三）
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2019/10/27/javaForKafka3/" class="article-date">
  <time datetime="2019-10-27T11:37:22.000Z" itemprop="datePublished">2019-10-27</time>
</a>   
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">4k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">14 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <p>kafka学习笔记（三）</p>
<a id="more"></a>

<h1 id="日志的清除策略以及压缩策略"><a href="#日志的清除策略以及压缩策略" class="headerlink" title="日志的清除策略以及压缩策略"></a>日志的清除策略以及压缩策略</h1><h2 id="日志清除策略"><a href="#日志清除策略" class="headerlink" title="日志清除策略"></a>日志清除策略</h2><p>日志的分段存储，一方面能够减少单个文件内容的大小，另一方面，方便 kafka 进行日志清理。日志的清理策略有两个</p>
<ol>
<li>根据消息的保留时间，当消息在 kafka 中保存的时间超过了指定的时间，就会触发清理过程</li>
<li>根据 topic 存储的数据大小，当 topic 所占的日志文件大小大于一定的阀值，则可以开始删除最旧的消息。kafka会启动一个后台线程，定期检查是否存在可以删除的消息<blockquote>
<p>通过<code>log.retention.bytes</code>和<code>log.retention.hours</code>这两个参数来设置，当其中任意一个达到要求，都会执行删除。默认的保留时间是：7 天</p>
</blockquote>
</li>
</ol>
<h2 id="日志压缩策略"><a href="#日志压缩策略" class="headerlink" title="日志压缩策略"></a>日志压缩策略</h2><p>Kafka 还提供了”日志压缩（Log Compaction）”功能，通过这个功能可以有效的减少日志文件的大小，缓解磁盘紧张的情况，在很多实际场景中，消息的key和value的值之间的对应关系是不断变化的，就像数据库中的数据会不断被修改一样，消费者只关心 key 对应的最新的 value。因此，我们可以开启 kafka 的日志压缩功能，服务端会在后台启动启动Cleaner 线程池，定期将相同的 key 进行合并，只保留最新的 value 值。<br>日志的压缩原理如图所示<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-log-compress.png" alt="log"></p>
<h1 id="partition-的高可用副本机制"><a href="#partition-的高可用副本机制" class="headerlink" title="partition 的高可用副本机制"></a>partition 的高可用副本机制</h1><p>Kafka的每个topic都可以分为多个Partition，并且多个 partition 会均匀分布在集群的各个节点下。虽然这种方式能够有效的对数据进行分片，但是对于每个partition来说，都是单点的，当其中一个 partition 不可用的时候，那么这部分消息就没办法消费。所以 kafka 为了提高 partition 的可靠性而提供了副本的概念（Replica）,通过副本机制来实现冗余备份。</p>
<h2 id="副本"><a href="#副本" class="headerlink" title="副本"></a>副本</h2><p>Kafka对消息进行了冗余备份，每个Partiton可以有多个副本，每个副本中包含的消息是一样的。每个分区至少有一个副本，当分区只有一个副本时，就只有Leader副本，没有Fellower副本。<br>每个分区的副本集合中，都会选举出一个副本作为Leader副本，所有的读写请求都是由选举出的Leader副本进行处理，其他都是Follower副本，Follower副本仅仅是从Leader副本处把数据拉取到本地之后，副本集会<br>存在一主多从的关系。<br>同步更新到自己的Log中。<br><img src="https://blog-dxysun.nos-eastchina1.126.net/copy.png" alt="copy"><br>一般情况下，同一个分区的多个副本会被均匀分配到集群中的不同broker 上，当leader 副本所在的 broker 出现故障后，可以重新选举新的 leader 副本继续对外提供服务。通过这样的副本机制来提高 kafka 集群的可用性。</p>
<h2 id="副本分配算法"><a href="#副本分配算法" class="headerlink" title="副本分配算法"></a>副本分配算法</h2><p>将所有N个Broker和待分配的i个Partition排序。将第i个Partition分配到第(i mod n)个Broker上。将第i个Partition的第j个副本分配到第((i + j) mod n)个Broker 上。</p>
<h2 id="kafka-副本机制中的概念"><a href="#kafka-副本机制中的概念" class="headerlink" title="kafka 副本机制中的概念"></a>kafka 副本机制中的概念</h2><p>Kafka 分区下有可能有很多个副本(replica)用于实现冗余，从而进一步实现高可用。副本根据角色的不同可分为 3 类：<br>leader 副本：响应 clients 端读写请求的副本。<br>follower 副本：被动地备份 leader 副本中的数据，不能响应 clients 端读写请求。<br>ISR 副本：包含了 leader 副本和所有与 leader 副本保持同步的 follower 副本。</p>
<p>如何判定是否与 leader 同步，与此紧密相关是每个 Kafka 副本对象的两个重要的属性：LEO 和 HW 。注意是所有的副本，而不只是 leader 副本。</p>
<p>LEO：即日志末端位移(log end offset)，记录了该副本底层日志(log)中下一条消息的位移值。注意是下一条消息！也就是说，如果 LEO=10，那么表示该副本保存了10条消息，位移值范围是[0, 9]。另外，leader LEO 和 follower LEO 的更新是有区别的。</p>
<p>HW：水位值，对于同一个副本对象而言，其HW 值不会大于LEO值。小于等于 HW 值的所有消息都被认为是”已备份”的（replicated）。同理，leader副本和follower副本的 HW更新也是有区别的</p>
<h2 id="副本协同机制"><a href="#副本协同机制" class="headerlink" title="副本协同机制"></a>副本协同机制</h2><p>消息的读写操作都只会由leader节点来接收和处理，follower副本只负责同步数据以及当leader副本所在的broker挂了以后，会从follower副本中选取新的leader。<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-copy1.png" alt="copy"></p>
<p>写请求首先由 Leader 副本处理，之后 follower 副本会从leader上拉取写入的消息，这个过程会有一定的延迟，导致follower副本中保存的消息略少于 leader 副本，但是只要没有超出阈值都可以容忍。但是如果一个 follower副本出现异常，比如宕机、网络断开等原因长时间没有同步到消息，那这个时候，leader 就会把它踢出去。kafka 通过ISR集合来维护一个分区副本信息。</p>
<h2 id="ISR"><a href="#ISR" class="headerlink" title="ISR"></a>ISR</h2><p>ISR 表示目前”可用且消息量与leader相差不多的副本集合，这是整个副本集合的一个子集”。具体来说，ISR 集合中的副本必须满足两个条件</p>
<ol>
<li>副本所在节点必须维持着与 zookeeper 的连接</li>
<li>副本最后一条消息的 offset 与 leader 副本的最后一条消息的offset之间的差值不能超过指定的阈值(replica.lag.time.max.ms)<br>replica.lag.time.max.ms：如果该 follower 在此时间间隔内一直没有追上过leader的所有消息，则该follower就会被剔除ISR列表</li>
</ol>
<p>ISR 数 据 保 存 在 Zookeeper 的<code>/brokers/topics/&lt;topic&gt;/partitions/&lt;partitionId&gt;/state</code>节点中</p>
<h2 id="HW-amp-LEO"><a href="#HW-amp-LEO" class="headerlink" title="HW&amp;LEO"></a>HW&amp;LEO</h2><p>关于follower副本同步的过程中，还有两个关键的概念，HW(HighWatermark)和 LEO(Log End Offset). 这两个参数跟ISR集合紧密关联。<br>HW标记了一个特殊的 offset，当消费者处理消息的时候，只能拉去到 HW 之前的消息，HW之后的消息对消费者来说是不可见的。<br>也就是说，取 partition 对应ISR中最小的LEO作为HW，consumer 最多只能消费到HW 所在的位置。每个replica都有 HW，leader 和 follower 各自维护更新自己的 HW 的状态。一条消息只有被ISR里的所有Follower都从Leader复制过去才会被认为已提交。这样就避免了部分数据被写进了Leader，还没来得及被任何 Follower 复制就宕机了，而造成数据丢失（Consumer 无法消费这些数据）。而对于Producer而言，它可以选择是否等待消息commit，这可以通过 acks 来设置。这种机制确保了只要ISR有一个或以上的 Follower，一条被 commit 的消息就不会丢失。</p>
<h2 id="数据的同步过程"><a href="#数据的同步过程" class="headerlink" title="数据的同步过程"></a>数据的同步过程</h2><p>数据的同步过程需要解决</p>
<ol>
<li>怎么传播消息</li>
<li>在向消息发送端返回ack之前需要保证多少个Replica已经接收到这个消息</li>
</ol>
<p>数据的处理过程<br>Producer 在发布消息到某个Partition时，先通过ZooKeeper 找到该 Partition 的 Leader[get/brokers/topics/<topic>/partitions/2/state]，然后无论该Topic 的 Replication Factor 为多少（也即该 Partition 有多少个 Replica），Producer 只将该消息发送到该 Partition 的Leader。Leader 会将该消息写入其本地 Log。每个 Follower都从 Leader pull 数据。这种方式上，Follower 存储的数据顺序与 Leader 保持一致。Follower 在收到该消息并写入其 Log 后，向Leader发送 ACK。一旦Leader收到了ISR中的所有Replica的ACK，该消息就被认为已经commit了，Leader 将增加 HW(HighWatermark)并且向 Producer 发送ACK。</p>
<p>初始状态<br>初始状态下，leader 和 follower 的 HW 和 LEO 都是 0，leader 副本会保存 remote LEO，表示所有 follower LEO，也会被初始化为 0，这个时候，producer 没有发送消息。<br>follower 会不断地个 leader 发送 fetch 请求，但是因为没有数据，这个请求会被 leader 寄存，当在指定的时间之后会强制完成请求，这个时间配置是(replica.fetch.wait.max.ms)，如果在指定时间内 producer有消息发送过来，那么 kafka 会唤醒 fetch 请求，让 leader继续处理。<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-fecth.png" alt="fecth"></p>
<p>这里会分两种情况，第一种是 leader 处理完 producer 请求之后，follower 发送一个 fetch 请求过来<br>第二种是follower 阻塞在 leader 指定时间之内，leader 副本收到 producer 的请求。<br>这两种情况下处理方式是不一样的。</p>
<p>第一种情况，follower 的 fetch 请求是当 leader 处理消息以后执行的 </p>
<p>生产者发送一条消息<br>leader 处理完 producer 请求之后，follower 发送一个 fetch 请求过来。状态图如下<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-fecth1.png" alt="fecth1"><br>leader 副本收到请求以后，会做几件事情</p>
<ol>
<li>把消息追加到 log 文件，同时更新 leader 副本的 LEO</li>
<li>尝试更新 leader HW 值。这个时候由于 follower 副本还没有发送 fetch 请求，那么 leader 的 remote LEO 仍然是0。leader 会比较自己的 LEO 以及 remote LEO 的值发现最小值是 0，与 HW 的值相同，所以不会更新 HW。</li>
</ol>
<p>follower fetch 消息<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-fecth2.png" alt="fecth"><br>follower 发送 fetch 请求，leader 副本的处理逻辑是:</p>
<ol>
<li>读取 log 数据、更新 remote LEO=0(follower 还没有写入这条消息，这个值是根据 follower 的 fetch 请求中的offset 来确定的)</li>
<li>尝试更新 HW，因为这个时候 LEO 和 remoteLEO 还是不一致，所以仍然是 HW=0</li>
<li>把消息内容和当前分区的 HW 值发送给 follower 副本</li>
</ol>
<p>follower 副本收到 response 以后</p>
<ol>
<li>将消息写入到本地 log，同时更新 follower 的 LEO 为 1</li>
<li>更新 follower HW，本地的 LEO 和 leader 返回的 HW 进行比较取小的值，所以仍然是 0</li>
</ol>
<p>第一次交互结束以后，HW 仍然还是 0，这个值会在下一次follower 发起 fetch 请求时被更新</p>
<p><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-fecth3.png" alt="fecth3"><br>follower 发第二次 fetch 请求，leader 收到请求以后</p>
<ol>
<li>读取 log 数据</li>
<li>更新 remote LEO=1， 因为这次 fetch 携带的 offset 是</li>
<li>更新当前分区的 HW，这个时候 leader LEO 和 remote LEO 都是 1，所以 HW 的值也更新为 1</li>
<li>把数据和当前分区的 HW 值返回给 follower 副本，这个时候如果没有数据，则返回为空</li>
</ol>
<p>follower 副本收到 response 以后</p>
<ol>
<li>如果有数据则写本地日志，则更新 LEO</li>
<li>更新 follower 的 HW 值到目前为止，数据的同步就完成了，意味着消费端能够消费 offset=0 这条消息。</li>
</ol>
<p>follower 的 fetch 请求是直接从阻塞过程中触发<br>如之前前面，由于 leader 副本暂时没有数据过来，所以follower 的 fetch 会被阻塞，直到等待超时或者 leader 接收到新的数据。当 leader 收到请求以后会唤醒处于阻塞的fetch请求。处理过程基本上和前面说的一致</p>
<ol>
<li>leader 将消息写入本地日志，更新 Leader 的 LEO</li>
<li>唤醒 follower 的 fetch 请求</li>
<li>更新 HW</li>
</ol>
<p>kafka 使用 HW 和 LEO 的方式来实现副本数据的同步，本身是一个好的设计，但是在这个地方会存在一个数据丢失的问题，当然这个丢失只出现在特定的背景下。</p>
<p>数据丢失的问题<br>前提：min.insync.replicas=1 的时候。<br>设定 ISR 中的最小副本数是多少，默认值为 1, 当且仅当 acks 参数设置为-1 （表示需要所有副本确认）时，此参数才生效。<br>表达的含义是，至少需要多少个副本同步才能表示消息是提交的<br>所以，当 min.insync.replicas=1 的时候一旦消息被写入 leader 端 log 即被认为是”已提交”，而延迟一轮 FETCH RPC 更新HW值的设计使得 follower HW值是异步延迟更新的，倘若在这个过程中 leader 发生变更，<br>那么成为新 leader 的 follower 的 HW 值就有可能是过期的，使得 clients 端认为是成功提交的消息被删除。<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-fecth4.png" alt="fecth4"></p>
<p>数据丢失的解决方案<br>在 kafka0.11.0.0 版本以后，提供了一个新的解决方案，使用 leader epoch 来解决这个问题，leader epoch 实际上是一对值(epoch,offset), epoch 表示 leader 的版本号，从 0 开始，当 leader 变更过 1 次时 epoch 就会+1，而 offset 则对应于该 epoch 版本的 leader 写入第一条消息的位移。比如说(0,0);(1,50); 表示第一个 leader 从 offset=0 开始写消息，一共写了 50 条，第二个 leader 版本号是 1，从 50 条处开<br>始写消息。这个信息保存在对应分区的本地磁盘文件中，文件名为： /kafka-log/topic/leader-epochcheckpoint</p>
<p>leader broker 中会保存这样的一个缓存，并定期地写入到一个 checkpoint 文件中。当 leader 写 log 时它会尝试更新整个缓存——如果这个leader 首次写消息，则会在缓存中增加一个条目；否则就不做更新。而每次副本重新成为 leader 时会查询这部分缓存，获取出对应 leader 版本的 offset<br><img src="https://blog-dxysun.nos-eastchina1.126.net/kafka-fecth5.png" alt="fecth5"></p>
<p>如何处理所有的 Replica 不工作的情况<br>在 ISR 中至少有一个 follower 时，Kafka 可以确保已经 commit 的数据不丢失，但如果某个 Partition 的所有 Replica 都宕机了，就无法保证数据不丢失了。</p>
<ol>
<li>等待 ISR 中的任一个 Replica”活”过来，并且选它作为 Leader</li>
<li>选择第一个”活”过来的 Replica（不一定是 ISR 中的）作为 Leader</li>
</ol>
<p>这就需要在可用性和一致性当中作出一个简单的折衷。如果一定要等待 ISR 中的 Replica”活”过来，那不可用的时间就可能会相对较长。而且如果 ISR 中的所有 Replica 都无法”活”过来了，或者数据都丢失了，这个 Partition 将永远不可用。选择第一个”活”过来的 Replica 作为 Leader，而这个 Replica 不是 ISR 中的 Replica，那即使它并不保证已经包含了所有已 commit 的消息，它也会成为 Leader 而作为consumer 的数据源（前文有说明，所有读写都由 Leader完成）。</p>
<h2 id="ISR-的设计原理"><a href="#ISR-的设计原理" class="headerlink" title="ISR 的设计原理"></a>ISR 的设计原理</h2><p>在所有的分布式存储中，冗余备份是一种常见的设计方式，而常用的模式有同步复制和异步复制，按照 kafka 这个副本模型来说如果采用同步复制，那么需要要求所有能工作的 Follower 副<br>本都复制完，这条消息才会被认为提交成功，一旦有一个follower 副本出现故障，就会导致 HW 无法完成递增，消息就无法提交，消费者就获取不到消息。这种情况下，故障的Follower 副本会拖慢整个系统的性能，设置导致系统不可用如果采用异步复制，leader 副本收到生产者推送的消息后，就认为次消息提交成功。follower 副本则异步从 leader 副本同步。这种设计虽然避免了同步复制的问题，但是假设所有follower 副本的同步速度都比较慢他们保存的消息量远远落后于 leader 副本。而此时 leader 副本所在的 broker 突然宕机，则会重新选举新的 leader 副本，而新的 leader 副本中没有原来 leader 副本的消息。这就出现了消息的丢失。<br>kafka 权衡了同步和异步的两种策略，采用 ISR 集合，巧妙解决了两种方案的缺陷：当 follower 副本延迟过高，leader 副本则会把该 follower 副本提出 ISR 集合，消息依然可以快速提交。当 leader 副本所在的 broker 突然宕机，会优先将 ISR 集合中follower 副本选举为 leader，新 leader 副本包含了 HW 之前的全部消息，这样就避免了消息的丢失。</p>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://dxysun.com/2019/10/27/javaForKafka3/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/java/" rel="tag">java</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/kafka/" rel="tag">kafka</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/2019/12/29/gitForMergeCommit/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            git将几个commit点合并成一个commit点
          
        </div>
      </a>
    
    
      <a href="/2019/10/27/javaForKafka2/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">kafka学习笔记（二）</div>
      </a>
    
  </nav>

  
   
  
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2015-2024
        <i class="ri-heart-fill heart_icon"></i> dxysun
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
        <li>
          <a href="https://beian.miit.gov.cn" target="_black" rel="nofollow">豫ICP备17012675号-1</a>
        </li>
        
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="https://dxysun.com/static/logo.png" alt="迎着朝阳"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/photos">相册</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">友链</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/about">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    <div class="reward-item">
      <img class="reward-img" src="https://tu.dxysun.com/alipay-20201219151322.jpg">
      <span class="reward-type">支付宝</span>
    </div>
    
    
    <div class="reward-item">
      <img class="reward-img" src="https://tu.dxysun.com/weixin-20201219151346.png">
      <span class="reward-type">微信</span>
    </div>
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
      tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
          processEscapes: true,
          skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
      }
  });

  MathJax.Hub.Queue(function() {
      var all = MathJax.Hub.getAllJax(), i;
      for(i=0; i < all.length; i += 1) {
          all[i].SourceElement().parentNode.className += ' has-jax';
      }
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.6/unpacked/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
  var ayerConfig = {
    mathjax: true
  }
</script>

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


<script src="/js/dz.js"></script>



    
  </div>
</body>

</html>